package brokers;

import java.io.FileWriter;
import java.io.IOException;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.Collections;

import models.ExtendedCloudlet;
import models.SLA;

import org.cloudbus.cloudsim.DatacenterBroker;
import org.cloudbus.cloudsim.Log;
import org.cloudbus.cloudsim.Vm;
import org.cloudbus.cloudsim.core.CloudSim;
import org.cloudbus.cloudsim.core.CloudSimTags;
import org.cloudbus.cloudsim.core.SimEvent;
import org.cloudbus.cloudsim.lists.VmList;

import queue.PriorityQueue;
import simulation.Observation;
import util.Globals;
import util.TimeComparator;


public class SkeletonBroker extends DatacenterBroker {

	/** The cloudlets submitted. */
	protected int cloudletsSubmitted;

	/** The cloudlet list. */
	protected ArrayList<ExtendedCloudlet> mycloudletList;

	protected double endSimulationTime = 0;
	private boolean firstrun = true;
	protected boolean[] occupation = null;
	protected static double utilization = 0;
	protected static double instantUtilization = 0;
	protected static int totalVmNumber;

	private PriorityQueue pqueue = null;
	protected SLA sla1 = null;
	protected SLA sla2 = null;
	protected double priority1 = 0;
	protected double priority2 = 0;
	protected int total1 = 0;
	protected int total2 = 0;
	protected int met1 = 0;
	protected int met2 = 0;


	protected int originalCloudletListSize = 0;
	protected int sent = 0;
	protected static int returned = 0;
	protected static double startSimulationTime = 0;
	protected static double firstCloudletReturnTimeAfterStabilization = 0;
	protected static double exitTime = 0;


	// mean statistics
	protected double cumulativet1 = 0;
	protected double cumulativet2 = 0;
	protected double cumulativecpu1 = 0;
	protected double cumulativecpu2 = 0;


	// service times and observations tracking
	private ArrayList<Double> serviceTimes = new ArrayList<Double>();
	private ArrayList<Observation> observations = new ArrayList<Observation>();

	// errors
	int alert = 0;
	double distance = 0;
	double maxDistance = Double.MIN_NORMAL;

	protected SkeletonBroker(String name, double endSimulationtime, SLA sla1, SLA sla2) throws Exception {
		super(name);
		returned = 0;
		exitTime = 0;
		totalVmNumber = 0;
		utilization = 0;
		instantUtilization = 0;
		startSimulationTime = 0;
		firstCloudletReturnTimeAfterStabilization = 0;
		this.sla1 = sla1;
		this.sla2 = sla2;
		this.endSimulationTime = endSimulationtime;
	}


	public void submitCloudletList(ArrayList<ExtendedCloudlet> list){
		mycloudletList = new ArrayList<ExtendedCloudlet>();
		mycloudletList.addAll(list);
		if(Globals.DEBUG_VERBOSE || Globals.DEBUG_MINIMAL)
			System.out.println(getName() + " " + list.size() + " cloudlets submitted!" );
	}
	
	private synchronized void addVms(){
		totalVmNumber += getVmList().size();
	}


	/**
	 * Submit cloudlets to the created VMs with FCFS discipline
	 * but submits n cloudlet a time where n is the number of free
	 * hosts\VMs.
	 *
	 * @pre $none
	 * @post $none
	 */
	@Override
	protected void submitCloudlets() {

		if(Globals.DEBUG_VERBOSE)
			System.out.println("\nSUBMITCLOUDLETS(), simulated time: " + CloudSim.clock());

		// first broker and priorityqueue initialization
		if(firstrun){

			System.out.println("\n\n");
			addVms();
			if(Globals.DEBUG_VERBOSE)
				System.out.println(getName() + " " + mycloudletList.size() + " cloudlets!" );

			originalCloudletListSize = mycloudletList.size();
			Collections.sort(mycloudletList, new TimeComparator());
			pqueue = new PriorityQueue();

			occupation = new boolean[getVmsCreatedList().size()];
			for(int i = 0; i < occupation.length; i++)
				occupation[i] = false;

			firstrun = false;
		}

		// exit criterion with time bound 
		if(CloudSim.clock() >= endSimulationTime){
			exitTime = CloudSim.clock();

			if(Globals.DEBUG_VERBOSE || Globals.DEBUG_MINIMAL){
				System.out.println("Simulation Ended by " + getName() + " in return (CAUSE: time exceed)");
				System.out.println("Number of servers: " + getVmList().size());
				System.out.println("Effectively completed cloudlets: " + getCloudletReceivedList().size()+"\n\n");
			}

			finishExecution();
			CloudSim.stopSimulation();
			CloudSim.abruptallyTerminate();
			return;
		}

		int vmIndex = -1;

		// search for an empty virtual machine
		// and print VMs statuses
		for(int i = 0; i < getVmsCreatedList().size(); i++){
			if(occupation[i] == false){
				if(Globals.DEBUG_VERBOSE)
					System.out.println("VM # " + i + " is free");
				vmIndex = i;
			}
			else {
				if(Globals.DEBUG_VERBOSE)
					System.out.println("VM # " + i + " is occupied");
			}
		}

		pqueue.setCloudletList(mycloudletList);

		// serve next cloudlet
		while (mycloudletList.size() > 0) {

			ExtendedCloudlet cloudlet = pqueue.getNextCloudlet(CloudSim.clock(), priority1, priority2);

			//??????
			if(cloudlet.isFinished()){
				if(Globals.DEBUG_VERBOSE)
					System.out.println(cloudlet.getCloudletId() + "IS FINISHED");
				continue;
			}

			Vm vm;

			if (cloudlet.getVmId() == -1) { //if user didn't bind this cloudlet and it has not been executed yet
				vm = getVmsCreatedList().get(vmIndex);
			} else { //submit to the specific vm
				vm = VmList.getById(getVmsCreatedList(), cloudlet.getVmId());
				if (vm == null) { // vm was not created
					if(Globals.DEBUG_VERBOSE)
						Log.printLine(CloudSim.clock()+": "+getName()+ ": Postponing execution of cloudlet "+cloudlet.getCloudletId()+": bount VM not available");
					continue;
				}
			}

			double delay = cloudlet.getActualSubmitTime() - CloudSim.clock();
			cloudlet.setVmId(vm.getId());

			if(Globals.DEBUG_VERBOSE || Globals.DEBUG_MINIMAL)
				Log.printLine(CloudSim.clock()+": "+getName()+ ": Sending cloudlet "+cloudlet.getCloudletId()+" (class " + cloudlet.getClassType() + ") to VM # "+vm.getId() 
						+ /*" in host # " + getVmsCreatedList().get(vmIndex).getHost().getId() + */ "\nwith " + delay + " delay (" + cloudlet.getExecStartTime() + ") and cloudletLength: " + cloudlet.getCloudletLength() + "\n");

			if(delay < 0)
				delay = 0;

			send(getVmsToDatacentersMap().get(vm.getId()), delay , CloudSimTags.CLOUDLET_SUBMIT, cloudlet);
			sent++;
			getCloudletSubmittedList().add(cloudlet);
			mycloudletList.remove(cloudlet);

			if(Globals.DEBUG_SINTETIC_CLOULET_SUBMIT_AND_RETURN)
				System.out.println(CloudSim.clock() + "] ::SUBMIT:: ID: (" + cloudlet.getCloudletId() + "), length: "+ cloudlet.getCloudletLength() + ", actual: " + cloudlet.getActualSubmitTime() + ", start: " + cloudlet.getExecStartTime());


			// set the assigned virtual machine as occupied

			occupation[vmIndex] = true;

			cloudletsSubmitted++;

			vmIndex = -1;

			// search for the fist empty virtual machine
			for(int i = 0; i < getVmsCreatedList().size(); i++){
				if(occupation[i] == false){
					vmIndex = i;
					break;
				}
			}

			// if no empty virtual machine exit (and wait)
			if(vmIndex < 0){
				if(Globals.DEBUG_VERBOSE)
					System.out.println();
				break;
			}

		}


	}


	/**
	 * Process a cloudlet return event.
	 *
	 * @param ev a SimEvent object
	 *
	 * @pre ev != $null
	 * @post $none
	 */
	@Override
	protected void processCloudletReturn(SimEvent ev) {

		ExtendedCloudlet cloudlet = (ExtendedCloudlet) ev.getData();

		if(Globals.DEBUG_VERBOSE || Globals.DEBUG_SLA_ONLY)
			Log.printLine(CloudSim.clock()+": "+getName()+ ": Cloudlet " +cloudlet.getCloudletId()+ " (class " + cloudlet.getClassType() + ") received from VM # " + cloudlet.getVmId() 
					+ " PE: " + VmList.getById(getVmsCreatedList(), cloudlet.getVmId()).getHost().getPeList().get(0).getId() 
					+ " HOST : " + VmList.getById(getVmsCreatedList(), cloudlet.getVmId()).getHost().getId() 
					+ " PE MIPS: " + VmList.getById(getVmsCreatedList(), cloudlet.getVmId()).getHost().getPeList().get(0).getMips());


		//DecimalFormat dft = new DecimalFormat("0.0000");
		//Itecs.write(dft.format(CloudSim.clock()) + "\t" + cloudlet.getCloudletId() + "\t" + cloudlet.getClassType() + "\t" + cloudlet.getCloudletLength() + "\t" + dft.format(cloudlet.getActualCPUTime()) + "\t" + pqueue.getQueueSize(CloudSim.clock()) + "\t" + cloudlet.getVmId());


		if(Globals.DEBUG_VERBOSE)
			System.out.println("Priority queue size: " + pqueue.getQueueSize(CloudSim.clock()));

		if(Globals.DEBUG_SINTETIC_CLOULET_SUBMIT_AND_RETURN)
			System.out.println(CloudSim.clock() + "]     ::RETURN:: ID: (" + cloudlet.getCloudletId() + "), length: "+ cloudlet.getCloudletLength() + ", cpu time: " + cloudlet.getActualCPUTime() + ", actual: " + cloudlet.getActualSubmitTime() + ", start: " + cloudlet.getExecStartTime());

		// exit criterion
		if(CloudSim.clock() >= endSimulationTime){
			exitTime = CloudSim.clock();
			if(Globals.DEBUG_VERBOSE || Globals.DEBUG_MINIMAL){
				System.out.println("Simulation Ended by " + getName() + " in return (CAUSE: time exceed)");
				System.out.println("Number of servers: " + getVmList().size());
				System.out.println("Effectively completed cloudlets: " + getCloudletReceivedList().size()+"\n\n");
			}

			finishExecution();
			CloudSim.stopSimulation();
			CloudSim.abruptallyTerminate();
			return;
		}

		getCloudletReceivedList().add(cloudlet);
		
		int vmIndex=0;
		for (int i=0; i<getVmsCreatedList().size(); i++){
			if (getVmsCreatedList().get(i).getId()==cloudlet.getVmId()){
				vmIndex=i;
				break;
			}
		}
		
		


		occupation[vmIndex] = false;
		
		synchronized(CloudSim.getSimulationCalendar()){
			returned++;
		}

		// CPU time error tracking
		if (((cloudlet.getCloudletLength() / Globals.VM_MIPS) + 0.1) <= cloudlet.getActualCPUTime()	|| ((cloudlet.getCloudletLength() / Globals.VM_MIPS) - 0.1) >= cloudlet.getActualCPUTime()) {
			if (Globals.DEBUG_VERBOSE || Globals.DEBUG_ALERT)
				System.out.println("ALERT!! "+ (cloudlet.getCloudletLength() / Globals.VM_MIPS)	+ " !=!= " + cloudlet.getActualCPUTime());
			alert++;
			distance += cloudlet.getCloudletLength() /Globals.VM_MIPS- cloudlet.getActualCPUTime();
			if (maxDistance < Math.abs(cloudlet.getCloudletLength()	/ Globals.VM_MIPS - cloudlet.getActualCPUTime())) {
				maxDistance = cloudlet.getCloudletLength() / Globals.VM_MIPS- cloudlet.getActualCPUTime();
			}
		}

		// update stats for class 1
		if(cloudlet.getClassType() == 1){
			cumulativet1 += (CloudSim.clock() - cloudlet.getActualSubmitTime());
			cumulativecpu1 += cloudlet.getActualCPUTime();
			total1++;

			if((CloudSim.clock() - cloudlet.getActualSubmitTime()) <= sla1.getX()){
				met1++;
				if(Globals.DEBUG_VERBOSE || Globals.DEBUG_SLA_ONLY)
					System.out.println("SLA-1 MET with x: " + sla1.getX() + " and service time: " + (CloudSim.clock() - cloudlet.getActualSubmitTime()) + " cpu time: " + cloudlet.getActualCPUTime());
			}
			else {
				if(Globals.DEBUG_VERBOSE || Globals.DEBUG_SLA_ONLY)
					System.out.println("SLA-1 _NOT_ MET with x: " + sla1.getX() + " and service time: " + (CloudSim.clock() - cloudlet.getActualSubmitTime()) + " cpu time: " + cloudlet.getActualCPUTime());
			}

		}

		// update stats for class 2
		if(cloudlet.getClassType() == 2){
			cumulativet2 += (CloudSim.clock() - cloudlet.getActualSubmitTime());
			cumulativecpu2 += cloudlet.getActualCPUTime();
			total2++;

			if((CloudSim.clock() - cloudlet.getActualSubmitTime()) <= sla2.getX()){
				met2++;
				if(Globals.DEBUG_VERBOSE || Globals.DEBUG_SLA_ONLY)
					System.out.println("SLA-2 MET with x: " + sla2.getX() + " and service time: " + (CloudSim.clock() - cloudlet.getActualSubmitTime()) + " cpu time: " + cloudlet.getActualCPUTime());
			}
			else {
				if(Globals.DEBUG_VERBOSE || Globals.DEBUG_SLA_ONLY)
					System.out.println("SLA-2 _NOT_ MET with x: " + sla2.getX() + " and service time: " + (CloudSim.clock() - cloudlet.getActualSubmitTime()) + " cpu time: " + cloudlet.getActualCPUTime());
			}
		}

		instantUtilization += cloudlet.getActualCPUTime();

		if(returned > Globals.SIMULATION_STABILIZATION_CLOUDLETS)
			utilization += cloudlet.getActualCPUTime();


		double iU = instantUtilization / CloudSim.clock();
		iU/=totalVmNumber;
		//System.out.println("T : " + totalVmNumber);


		serviceTimes.add(new Double(CloudSim.clock() - cloudlet.getActualSubmitTime()));
		observations.add(new Observation(CloudSim.clock(),cloudlet.getCloudletId(), cloudlet.getClassType(), CloudSim.clock() - cloudlet.getActualSubmitTime(), (double)pqueue.getQueueSize(CloudSim.clock()), iU));

		if(Globals.DEBUG_SLA_ONLY)
			System.out.println();

		cloudletsSubmitted--;

		if(returned == Globals.SIMULATION_STABILIZATION_CLOUDLETS + 1){
			startSimulationTime = cloudlet.getActualSubmitTime();
			firstCloudletReturnTimeAfterStabilization = CloudSim.clock();
		}



		// exit criterion with cloudlet return bound
		if(returned == Globals.SIMULATION_STABILIZATION_CLOUDLETS + Globals.SIMULATION_CLOUDLETS){
			exitTime = CloudSim.clock();

			if(Globals.DEBUG_VERBOSE || Globals.DEBUG_MINIMAL){
				System.out.println("Simulation Ended by " + getName() + " in return (CAUSE: desired number of observations done)");
				System.out.println("Number of servers: " + getVmList().size());
				System.out.println("Effectively completed cloudlets: " + getCloudletReceivedList().size()+"\n\n");
			}

			finishExecution();
			CloudSim.stopSimulation();
			CloudSim.abruptallyTerminate();
			return;
		}


		if (mycloudletList.size()==0&&cloudletsSubmitted==0) { //all cloudlets executed
			Log.printLine(CloudSim.clock()+": "+getName()+ ": All Cloudlets executed. Finishing...");
			clearDatacenters();
			finishExecution();
		} else { //some cloudlets haven't finished yet
			if (mycloudletList.size()>0 && cloudletsSubmitted==0) {
				if(Globals.DEBUG_VERBOSE)
					System.out.println("X_mycloudletList.size()>0 && cloudletsSubmitted==0_X");
				//all the cloudlets sent finished. It means that some bount
				//cloudlet is waiting its VM be created
				//clearDatacenters();
				//createVmsInDatacenter(0);
			}
		}	

	}



	/**
	 * Send an internal event communicating the end of the simulation.
	 *
	 * @pre $none
	 * @post $none
	 */
	protected void finishExecution() {
		sendNow(getId(), CloudSimTags.END_OF_SIMULATION);
	}


	public boolean SLAsAreMet(){
		double p1 = (double)met1/(double)total1;
		double p2 = (double)met2/(double)total2;

		if (total1==0){
			return (p2 >= sla2.getY());
		}
		if (total2==0){
			return (p1 >= sla1.getY()) ;
		}

		boolean metsla = (p1 >= sla1.getY()) && (p2 >= sla2.getY());

		if(Globals.DEBUG_SLA_ONLY)
			System.out.println("metsla: " + metsla);

		return metsla;
	}


	public void getOrderedServiceTimes(){

		Collections.sort(serviceTimes);
		DecimalFormat dft = new DecimalFormat("#0.00");


		for(int i = 0; i < serviceTimes.size(); i++){
			System.out.print(i + ") " + serviceTimes.get(i).doubleValue() + " " + dft.format(((double) (i+1)/serviceTimes.size())*100) + "% ");

			if((((double) (i+1)/serviceTimes.size())) <= sla1.getY()){
				System.out.print(" <- sla1 " + sla1.getY());
			}

			if((((double) (i+1)/serviceTimes.size())) <= sla2.getY()){
				System.out.print(" <- sla2 " + sla2.getY());
			}

			System.out.println();
		}

	}


	public int getMaxQueueSize() {
		return pqueue.getMaxSize();
	}


	public ArrayList<Observation> getObservations() {
		return observations;
	}


	public double getRunThroughput() {
		return (returned - Globals.SIMULATION_STABILIZATION_CLOUDLETS)/ (exitTime - startSimulationTime);
	}

	public double getUtilization() {
		double U = utilization;
		U /= totalVmNumber;
		U /= exitTime - startSimulationTime;		
		return U;
	}


	public void printStatistics(){
		DecimalFormat dft = new DecimalFormat("##0.000");

		System.out.println();
		System.out.println("************** BROKER STATISTICS **************");
		System.out.println("Number of servers: " + getVmsCreatedList().size() + "");

		if(total1 > 0){
			System.out.println(getName() + " SLA1(" + sla1.getX() +", "+ sla1.getY() + ") total: " + total1 + " met: "+ met1 + ", missed: " + (total1-met1) + ", met-ratio: " + dft.format(((double)met1/(double)total1)*100) + "%" 
					+ (((double)met1/(double)total1) <= sla1.getY() ? " (NOT MET)" : " (MET)"));
			System.out.println("Class 1 Mean Service Time: " + dft.format(cumulativet1/total1));
			System.out.println("Class 1 Mean CPU Time: " + dft.format(cumulativecpu1/total1));
		}

		if(total2 > 0){
			System.out.println(getName() + " SLA2(" + sla2.getX() +", "+ sla2.getY() + ") total: " + total2 + " met: "+ met2 + ", missed: " + (total2-met2) + ", met-ratio: " + dft.format(((double)met2/(double)total2)*100) + "%"
					+ (((double)met2/(double)total2) <= sla2.getY() ? " (NOT MET)" : " (MET)"));
			System.out.println("Class 2 Mean Service Time: " + dft.format(cumulativet2/total2));
			System.out.println("Class 2 Mean CPU Time: " + dft.format(cumulativecpu2/total2));

		}

		System.out.println("Mean Cloudlet length: " + Globals.CLOUDLET_LENGTH_MEAN);
		System.out.println("Estimated MU: " + (1/Globals.CLOUDLET_LENGTH_MEAN)/Globals.VM_MIPS);

		System.out.println("Original Cloudlet list size: " + originalCloudletListSize);
		System.out.println("Served Cloudlets: " + (total1 + total2) + " (" + returned + "/"+ sent + ")" );
		System.out.println("First arrival instant after stabilization: " + startSimulationTime);
		System.out.println("First return instant after stabilization: " + firstCloudletReturnTimeAfterStabilization);
		System.out.println("Cloudlets Served in: " + dft.format(exitTime) +   " (limit exit time : " + Globals.SIMULATION_TIME + ")");
		System.out.println("Max queue size: " + pqueue.getMaxSize());
		System.out.println("Remaining to submit: " + mycloudletList.size());
		System.out.println("Not served: " + (originalCloudletListSize - (total1 + total2)));

		if(alert > 0)
			System.out.println("\nWARNING: " + alert + " ALERTS: mean distance: " + distance/alert + " max distance: " + maxDistance);

		System.out.println("***********************************************");
		System.out.println();
		
		
		try {
			FileWriter f = new FileWriter("output/mmmmm.csv", true);
			f.write(this.getName() + " \t " + dft.format(((double)met1/(double)total1)*100) + " \t " + dft.format(((double)met2/(double)total2)*100) + " \t " + dft.format(cumulativet1/total1) + " \t " + dft.format(cumulativet2/total2) + "\n");
			f.flush();
			f.close();
		} catch (IOException e) {
			e.printStackTrace();
		}

		//getOrderedServiceTimes();
	}


}
